package com.zusmart.base.buffer.support;

import java.nio.ByteBuffer;

import com.zusmart.base.buffer.Buffer;

public class LinkedBuffer extends AbstractBuffer {

	private final Entry head;

	public LinkedBuffer(Buffer[] content) {
		super(0, 0);
		this.head = new Entry();
		this.head.prev = this.head.next = this.head;
		for (int i = 0; i < content.length; i++) {
			this.append(content[i].duplicate());
		}
		this.limit(this.capacity());
	}

	private LinkedBuffer(Entry head, int offset, int capacity) {
		super(offset, capacity);
		this.head = head;
	}

	public Buffer appendBuffer(Buffer buffer) {
		this.append(buffer);
		return this;
	}

	public Buffer removeBuffer(Buffer buffer) {
		this.remove(buffer);
		return this;
	}

	@Override
	public boolean isDirect() {
		for (Entry e = this.head.next; e != this.head; e = e.next) {
			if (!e.buffer.isDirect()) {
				return false;
			}
		}
		return this.head.next != this.head; // is not empty
	}

	@Override
	public Buffer compact() {
		this.checkReadOnly();
		Buffer buffer = ByteArrayBuffer.allocate(this.remaining());
		batch(true, this.getIndex(0), buffer, buffer.capacity());
		buffer.position(0);
		int index = this.getIndex(0, 0);
		Entry entry = this.getEntry(index);
		int offset = index - entry.position;
		do {
			Buffer content = entry.buffer;
			buffer.limit(Math.min(buffer.capacity(), buffer.position() + content.remaining() - offset));
			content.put(content.position() + offset, buffer);
			if (buffer.position() == buffer.capacity()) {
				break;
			}
			offset = 0;
		} while ((entry = entry.next) != this.head);
		buffer.release();
		position(this.remaining());
		limit(this.capacity());
		return this;
	}

	@Override
	public Buffer slice() {
		return new DelegateBuffer(this.getIndex(0), this.remaining()).setReadOnly(this.isReadOnly());
	}

	@Override
	public Buffer duplicate() {
		LinkedBuffer buffer = new DelegateBuffer(this.getIndex(0, 0), this.capacity());
		buffer.limit(this.limit()).position(this.position());
		buffer.mark(this.getMark());
		buffer.setReadOnly(this.isReadOnly());
		return buffer;
	}

	@Override
	public ByteBuffer asByteBuffer() {
		ByteBuffer buffer = ByteBuffer.allocate(this.capacity());
		batch(true, this.getIndex(0, 0), buffer, buffer.capacity());
		buffer.position(this.position());
		buffer.limit(this.limit());
		return buffer;
	}

	@Override
	protected byte doGet(int index) {
		Entry entry = getEntry(index);
		Buffer buffer = entry.buffer;
		return buffer.get(buffer.position() + index - entry.position);
	}

	@Override
	protected void doPut(int index, byte b) {
		Entry entry = getEntry(index);
		Buffer buffer = entry.buffer;
		buffer.put(buffer.position() + index - entry.position, b);
	}

	@Override
	protected void doRelease() {
		for (Entry e = this.head.next; e != this.head;) {
			e.buffer.release();
			e.buffer = null;
			e.prev = null;
			e = e.next;
			e.prev.next = null;
		}
		this.head.next = this.head.prev = this.head;
	}

	@Override
	public Buffer get(byte[] dst, int offset, int length) {
		checkBounds(offset, length, dst.length);
		return batch(true, this.getIndex(length), dst, offset, length);
	}

	@Override
	public Buffer get(int index, byte[] dst, int offset, int length) {
		checkBounds(offset, length, dst.length);
		return batch(true, this.getIndex(index, length), dst, offset, length);
	}

	@Override
	public Buffer get(ByteBuffer dst, int length) {
		checkBounds(0, length, dst.remaining());
		return batch(true, this.getIndex(length), dst, length);
	}

	@Override
	public Buffer get(int index, ByteBuffer dst, int length) {
		checkBounds(0, length, dst.remaining());
		return batch(true, this.getIndex(index, length), dst, length);
	}

	@Override
	public Buffer get(Buffer dst, int length) {
		checkBounds(0, length, dst.remaining());
		return batch(true, this.getIndex(length), dst, length);
	}

	@Override
	public Buffer get(int index, Buffer dst, int length) {
		checkBounds(0, length, dst.remaining());
		return batch(true, this.getIndex(index, length), dst, length);
	}

	@Override
	public Buffer put(byte[] src, int offset, int length) {
		checkBounds(offset, length, src.length);
		return batch(false, this.putIndex(length), src, offset, length);
	}

	@Override
	public Buffer put(int index, byte[] src, int offset, int length) {
		checkBounds(offset, length, src.length);
		return batch(false, this.putIndex(index, length), src, offset, length);
	}

	@Override
	public Buffer put(ByteBuffer src, int length) {
		checkBounds(0, length, src.remaining());
		return batch(false, this.putIndex(length), src, length);
	}

	@Override
	public Buffer put(int index, ByteBuffer src, int length) {
		checkBounds(0, length, src.remaining());
		return batch(false, this.putIndex(index, length), src, length);
	}

	@Override
	public Buffer put(Buffer src, int length) {
		checkBounds(0, length, src.remaining());
		return batch(false, this.putIndex(length), src, length);
	}

	@Override
	public Buffer put(int index, Buffer src, int length) {
		checkBounds(0, length, src.remaining());
		return batch(false, this.putIndex(index, length), src, length);
	}

	@Override
	public short getShort() {
		return this.decodeShort(this.getIndex(2));
	}

	@Override
	public short getShort(int index) {
		return this.decodeShort(this.getIndex(index, 2));
	}

	@Override
	public Buffer putShort(short s) {
		return this.encodeShort(this.putIndex(2), s);
	}

	@Override
	public Buffer putShort(int index, short s) {
		return this.encodeShort(this.putIndex(index, 2), s);
	}

	@Override
	public int getInt() {
		return this.decodeInt(this.getIndex(4));
	}

	@Override
	public int getInt(int index) {
		return this.decodeInt(this.getIndex(index, 4));
	}

	@Override
	public Buffer putInt(int i) {
		return this.encodeInt(this.putIndex(4), i);
	}

	@Override
	public Buffer putInt(int index, int i) {
		return this.encodeInt(this.putIndex(index, 4), i);
	}

	@Override
	public long getLong() {
		return this.decodeLong(this.getIndex(8));
	}

	@Override
	public long getLong(int index) {
		return this.decodeLong(this.getIndex(index, 8));
	}

	@Override
	public Buffer putLong(long l) {
		return this.encodeLong(this.putIndex(8), l);
	}

	@Override
	public Buffer putLong(int index, long l) {
		return this.encodeLong(this.putIndex(index, 8), l);
	}

	private short decodeShort(int index) {
		Entry entry = this.getEntry(index);
		Buffer buffer = entry.buffer;
		int idx = buffer.position() + index - entry.position;
		if (buffer.limit() - idx >= 2) {
			return buffer.getShort(idx);
		}
		return AbstractBuffer.decodeShort(this, index);
	}

	private Buffer encodeShort(int index, short s) {
		Entry entry = this.getEntry(index);
		Buffer buffer = entry.buffer;

		int idx = buffer.position() + index - entry.position;
		if (buffer.limit() - idx >= 2) {
			buffer.putShort(idx, s);
		} else {
			AbstractBuffer.encodeShort(this, index, s);
		}
		return this;
	}

	private long decodeLong(int index) {
		Entry entry = this.getEntry(index);
		Buffer buffer = entry.buffer;
		int idx = buffer.position() + index - entry.position;
		if (buffer.limit() - idx >= 8) {
			return buffer.getLong(idx);
		}
		return AbstractBuffer.decodeLong(this, index);
	}

	private Buffer encodeLong(int index, long l) {
		Entry entry = this.getEntry(index);
		Buffer buffer = entry.buffer;
		int idx = buffer.position() + index - entry.position;
		if (buffer.limit() - idx >= 8) {
			buffer.putLong(idx, l);
		} else {
			AbstractBuffer.encodeLong(this, index, l);
		}
		return this;
	}

	private Entry getEntry(int index) {
		for (Entry e = this.head.prev; e != this.head; e = e.prev) {
			if (index >= e.position) {
				return e;
			}
		}
		return null;
	}

	protected Buffer batch(boolean get, int index, byte[] array, int offset, int length) {
		Entry entry = this.getEntry(index);
		int off = index - entry.position;
		do {
			Buffer buffer = entry.buffer;
			int len = Math.min(buffer.remaining() - off, length);
			if (get) {
				buffer.get(buffer.position() + off, array, offset, len);
			} else {
				buffer.put(buffer.position() + off, array, offset, len);
			}
			offset += len;
			length -= len;
			if (length <= 0) {
				break;
			}
			off = 0;
		} while ((entry = entry.next) != this.head);
		return this;
	}

	protected Buffer batch(boolean get, int index, ByteBuffer buffer, int length) {
		Entry entry = this.getEntry(index);
		int off = index - entry.position;
		do {
			Buffer content = entry.buffer;
			int len = Math.min(content.remaining() - off, length);
			if (get) {
				content.get(content.position() + off, buffer, len);
			} else {
				content.put(content.position() + off, buffer, len);
			}

			length -= len;
			if (length <= 0) {
				break;
			}
			off = 0;
		} while ((entry = entry.next) != this.head);
		return this;
	}

	protected Buffer batch(boolean get, int index, Buffer buffer, int length) {
		Entry entry = this.getEntry(index);
		int off = index - entry.position;
		do {
			Buffer content = entry.buffer;
			int len = Math.min(content.remaining() - off, length);
			if (get) {
				content.get(content.position() + off, buffer, len);
			} else {
				content.put(content.position() + off, buffer, len);
			}
			length -= len;
			if (length <= 0) {
				break;
			}
			off = 0;
		} while ((entry = entry.next) != this.head);
		return this;
	}

	private int decodeInt(int index) {
		Entry entry = this.getEntry(index);
		Buffer buffer = entry.buffer;
		int idx = buffer.position() + index - entry.position;
		if (buffer.limit() - idx >= 4) {
			return buffer.getInt(idx);
		}
		return AbstractBuffer.decodeInt(this, index);
	}

	private Buffer encodeInt(int index, int i) {
		Entry entry = this.getEntry(index);
		Buffer buffer = entry.buffer;
		int idx = buffer.position() + index - entry.position;
		if (buffer.limit() - idx >= 4) {
			buffer.putInt(idx, i);
		} else {
			AbstractBuffer.encodeInt(this, index, i);
		}
		return this;
	}

	private void remove(Buffer buffer) {
		for (Entry e = this.head.next; e != this.head; e = e.next) {
			if (e.buffer == buffer) {
				Entry removedEntry = e;
				int removedSize = e.buffer.remaining();
				e.prev.next = e.next;
				e.next.prev = e.prev;
				while ((e = e.next) != this.head) {
					e.position -= removedSize;
				}
				removedEntry.prev = null;
				removedEntry.next = null;
				removedEntry.buffer = null;
				capacity(this.capacity() - removedSize);
				limit(this.capacity());
				position(0);
				return;
			}
		}
	}

	private void append(Buffer buffer) {
		if (null != buffer) {
			Entry entry = new Entry();
			Entry last = this.head.prev;
			last.next = entry;
			entry.prev = last;
			entry.next = this.head;
			entry.buffer = buffer;
			entry.position = (last == this.head ? 0 : last.buffer.remaining()) + last.position;
			this.head.prev = entry;
			this.capacity(this.capacity() + buffer.remaining());
		}
	}

	private class DelegateBuffer extends LinkedBuffer {

		public DelegateBuffer(int offset, int capacity) {
			super(LinkedBuffer.this.head, offset, capacity);
		}

		@Override
		public boolean isReleased() {
			return LinkedBuffer.this.isReleased();
		}

		@Override
		public void release() {
			LinkedBuffer.this.release();
		}

		@Override
		public boolean isPermanent() {
			return LinkedBuffer.this.isPermanent();
		}

		@Override
		public Buffer setPermanent(boolean permanent) {
			return LinkedBuffer.this.setPermanent(permanent);
		}
	}

	private static final class Entry {
		public int position;
		public Buffer buffer;
		public Entry next;
		public Entry prev;
	}

}